<?php

require_once 'message.php';
require_once 'timeout.php';

class SocketParams
{
    protected $_M_host;
    protected $_M_port;
    protected $_M_timeout_connect;
    protected $_M_timeout_read_or_write;
    protected $_M_timeout_write;
    protected $_M_timeout_read;
    protected $_M_persistent;
    protected $_M_OnMessage;

    function __construct($host, $port)
    {
        $this->host($host);
        $this->port($port);
        $this->_M_timeout_connect       = TimeOut(10, 30);
        $this->timeout_read_or_write(TimeOut(10, 30));
        $this->_M_persistent            = false;
        $this->_M_OnMessage             = null;
    }

    function host()
    {
        $num_args = func_num_args();
        if (func_num_args() == 0) {
            return $this->_M_host;
        } elseif (func_num_args() == 1 && is_string(func_get_arg(0))) {
            return $this->_M_host = func_get_arg(0);
        } else {
            throw new Exception("Wrong parameters for `host()` of `SocketParams` object.");
        }
    }

    function OnMessageEvent()
    {
        $num_args = func_num_args();
        if (func_num_args() == 0) {
            return $this->_M_OnMessage;
        } elseif (func_num_args() == 1) {
            $param = func_get_arg(0);
            if (is_callable($param, is_array($param))) {
                return $this->_M_OnMessage = func_get_arg(0);
            } else {
                throw new Exception("Wrong parameters for `OnMessageEvent()` of `SocketParams` object.");
            }
        } else {
            throw new Exception("Wrong parameters for `OnMessageEvent()` of `SocketParams` object.");
        }
    }

    function port()
    {
        $num_args = func_num_args();
        if (func_num_args() == 0) {
            return $this->_M_port;
        } elseif (func_num_args() == 1 &&  is_numeric(func_get_arg(0))) {
            return $this->_M_port = intval(func_get_arg(0));
        } else {
            throw new Exception("Wrong parameters for `timeout()` of `SocketParams` object.");
        }
    }

    function persistent()
    {
        $num_args = func_num_args();
        if (func_num_args() == 0) {
            return $this->_M_persistent;
        } elseif (func_num_args() == 1) {
            return $this->_M_persistent = func_get_arg(0) ? true : false;
        } else {
            throw new Exception("Wrong parameters for `persistent()` of `SocketParams` object.");
        }
    }

    function timeout_connect()
    {
        $num_args = func_num_args();
        if (func_num_args() == 0) {
            return $this->_M_timeout_connect;
        } elseif ((func_num_args() == 1) && is_a(func_get_arg(0), 'TimeOut')) {
            return $this->_M_timeout_connect = func_get_arg(0);
        } else {
            throw new Exception("Wrong parameters for `timeout_connect()` of `SocketParams` object.");
        }
    }

    function timeout_read_or_write()
    {
        $num_args = func_num_args();
        if (func_num_args() == 0) {
            return $this->_M_timeout_read_or_write;
        } elseif ((func_num_args() == 1) && is_a(func_get_arg(0), 'TimeOut')) {
            $this->_M_timeout_read_or_write = func_get_arg(0);
            $this->_M_timeout_read = $this->_M_timeout_read_or_write;
            $this->_M_timeout_write = $this->_M_timeout_read_or_write;
            return $this->_M_timeout_read_or_write;
        } else {
            throw new Exception("Wrong parameters for `timeout_read_or_write()` of `SocketParams` object.");
        }
    }

    function timeout_read()
    {
        $num_args = func_num_args();
        if (func_num_args() == 0) {
            return $this->_M_timeout_read;
        } elseif ((func_num_args() == 1) && is_a(func_get_arg(0), 'TimeOut')) {
            return $this->_M_timeout_read = func_get_arg(0);
        } else {
            throw new Exception("Wrong parameters for `timeout_read()` of `SocketParams` object.");
        }
    }

    function timeout_write()
    {
        $num_args = func_num_args();
        if (func_num_args() == 0) {
            return $this->_M_timeout_write;
        } elseif ((func_num_args() == 1) && is_a(func_get_arg(0), 'TimeOut')) {
            return $this->_M_timeout_write = func_get_arg(0);
        } else {
            throw new Exception("Wrong parameters for `timeout_write()` of `SocketParams` object.");
        }
    }

}

function SocketParams($host, $port)
{
    return new SocketParams($host, $port);
}

class Socket
{

    protected $_M_sock;
    protected $_M_history;
    protected $_M_start;
    protected $_M_param;

    function __construct(SocketParams $param)
    {
        $this->_M_start = microtime(true);
        $this->_M_history = array();
        $this->_M_param = $param;

        $this->_M_sock = @fsockopen(
            $this->_M_param->host(),
            $this->_M_param->port(),
            $errno,
            $errstr,
            $this->_M_param->timeout_connect()->out
        );
        if (! $this->_M_sock) {
            throw new Exception();
        } else {

        }
    }

    function timeout_read_or_write()
    {
        $params = func_get_args();
        return call_user_func_array(array($this->_M_param, __FUNCTION__), $params);
    }

    function timeout_read()
    {
        $params = func_get_args();
        return call_user_func_array(array($this->_M_param, __FUNCTION__), $params);
    }

    function timeout_write()
    {
        $params = func_get_args();
        return call_user_func_array(array($this->_M_param, __FUNCTION__), $params);
    }

    function blocked()
    {
        $info = $this->state();
        if (isset($info['blocked'])) {
            return $info['blocked'];
        } else {
            throw new Exception();
        }
    }

    function state()
    {
        return stream_get_meta_data($this->_M_sock);
    }

    function __write($str)
    {
        $num = 0;
        if (func_num_args() == 1) {
            $num = fwrite($this->_M_sock, $str);
        } elseif (func_num_args() == 2) {
            $length = func_get_arg(1);
            $num = fwrite($this->_M_sock, $str, $length);
        } else {
            throw new Exception();
        }
        $this->history_add(new RequestMessage($str));
        return $num;
    }

    function __read($length)
    {
        return fread($this->_M_sock, $length);
    }

    function write($msg)
    {
        $start   = microtime(true);

        if (func_num_args() > 1) {
            $timeout = func_get_arg(1);
        } else {
            $timeout = $this->_M_param->timeout_write();
        }

        $timeout = $timeout->out;
        $msg_len = strlen($msg);
        $writed_msg = '';
        $sum_writed = 0;

        while (!$this->eof()) {
            $this->set_timeout($timeout);
            if (($num = $this->__write($msg)) !== false) {
                if ($num < $msg_len) {
                    $sum_writed += $num;
                    $msg = substr($msg, $num);
                    if ($this->timed_out()) {
                        if ($timeout < (microtime(true) - $start)) {
                            return $msg_len - $sum_writed;
                        } else {
                            continue;
                        }
                    }
                } else {
                    return 0;
                }
            } else {
                return $msg_len-$sum_writed;
            }
        }
    }

    function __destruct()
    {
        fclose($this->_M_sock);
    }

    function eof()
    {
        return feof($this->_M_sock);
    }

    function read()
    {
        $start = microtime(true);

        if (func_num_args()) {
            $timeout = func_get_arg(0);
        } else {
            $timeout = $this->_M_param->timeout_read();
        }

        $buff = '';
        $buff_size = 2048;
        $_timeout = min(0.1, $timeout->idle);
        do {
            $this->set_timeout($_timeout);
            $buff .= $this->__read($buff_size);

            $can_work = $timeout->idle > (microtime(true) - $start);
            $can_idle = $timeout->out > (microtime(true) - $start);
            $reading  = strlen($buff);
            $has_data = $this->has_data();

        } while ($can_work && (($can_idle xor $reading) || $has_data));

        if (strlen($buff)) {
            $this->history_add(new ResponseMessage($buff));
        }
    }

    function history_add($msg)
    {
        $this->_M_history[] = $msg;
        if (! is_null($feedback = $this->_M_param->OnMessageEvent())) {
            call_user_method($feedback[1], $feedback[0], $msg);
        }
    }

    function timed_out()
    {
        $state = $this->state();
        return !empty($state['timed_out']);
    }

    function has_data()
    {
        $state = $this->state();
        return empty($state['unread_bytes']) ? 0 : $state['unread_bytes'];
    }

    function set_timeout($seconds)
    {
        $_seconds      = intval(floor($seconds));
        $_microseconds = intval(round(($seconds - $_seconds) * 1000000));
        if (! @stream_set_timeout($this->_M_sock, $_seconds, $_microseconds)) {
            throw new Exception();
        }
    }

}

?>